package com.mxx.common.data.jpa;

import com.mxx.common.core.BTConsumer;
import com.mxx.common.data.jpa.auditing.DeleteFlagThreadLocal;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.QueryUtils;

import javax.persistence.criteria.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * @author itzgyw
 * @describe Jpa Specification条件构造工具
 */

public abstract class AbstractSpecHelper<E extends AbstractSpecHelper<E, K>, K> implements Builder<E> {


    protected Root root;
    protected CriteriaQuery criteriaQuery;
    protected CriteriaBuilder criteriaBuilder;
    protected List<Predicate> predicateList;
    protected Predicate cachePredicate;


    protected AbstractSpecHelper(Root root, CriteriaBuilder criteriaBuilder, CriteriaQuery criteriaQuery) {

        this.root = root;
        this.criteriaBuilder = criteriaBuilder;
        this.criteriaQuery = criteriaQuery;
        this.predicateList = new ArrayList<>();
    }

    public E orderBy(Sort sort) {
        criteriaQuery.orderBy(QueryUtils.toOrders(sort, root, criteriaBuilder));
        return this.getTarget();
    }

    public E ignoreDeleteFlag() {
        DeleteFlagThreadLocal.unUse();
        return this.getTarget();
    }

    public E useDeleteFlag() {
        DeleteFlagThreadLocal.use();
        return this.getTarget();
    }

    public E orderBy(Sort.Order sort) {
        return this.orderBy(Sort.by(sort));
    }

    /**
     * 等于
     *
     * @param key 字段名
     * @param val 对应值
     * @return
     */


    public E equal(K key, Object val) {
        predicateList.add(criteriaBuilder.equal(this.get(key), val));
        return this.getTarget();
    }


    /**
     * 逻辑判断
     *
     * @param condition 条件
     * @param yes       条件为 true 执行
     * @return
     */

    public E judge(boolean condition, Consumer<E> yes) {
        if (condition) {
            yes.accept(this.getTarget());
        }
        return this.getTarget();
    }

    /**
     * 逻辑判断
     *
     * @param condition 条件
     * @param yes       条件为 true 执行
     * @param no        条件为false 执行
     * @return
     */

    public E judge(boolean condition, Consumer<E> yes, Consumer<E> no) {
        if (condition) {
            yes.accept(this.getTarget());
        } else {
            no.accept(this.getTarget());
        }
        return this.getTarget();
    }

    /**
     * 遍历
     *
     * @param collection
     * @param each
     * @param <V>
     * @return
     */
    public <V> E each(Collection<V> collection, BiConsumer<E, V> each) {
        collection.forEach(v -> each.accept(this.getTarget(), v));
        return this.getTarget();
    }

    /**
     * 遍历
     *
     * @param map
     * @param each
     * @param <MK>
     * @param <MV>
     * @return
     */
    public <MK, MV> E each(Map<MK, MV> map, BTConsumer<E, MK, MV> each) {
        map.forEach((mk, mv) -> each.accept(this.getTarget(), mk, mv));
        return this.getTarget();
    }

    /**
     * 等于
     *
     * @param key      字段名
     * @param function 对数据类型进行转换
     * @param val      对应值
     * @return
     */
    public E equal(K key, Function<Path, Expression> function, Object val) {
        predicateList.add(criteriaBuilder.equal(function.apply(this.get(key)), val));
        return this.getTarget();
    }

    public E notEqual(K key, Object val) {
        predicateList.add(criteriaBuilder.notEqual(this.get(key), val));
        return this.getTarget();
    }

    public E notEqual(K key, Function<Path, Expression> function, K key2) {
        predicateList.add(criteriaBuilder.notEqual(function.apply(this.get(key)), key2));
        return this.getTarget();
    }

    public E leftLike(K key, String val) {
        predicateList.add(criteriaBuilder.like(this.get(key), "%" + val));
        return this.getTarget();
    }

    public E leftLike(K key, Function<Path, Expression> function, String val) {
        predicateList.add(criteriaBuilder.like(function.apply(this.get(key)), "%" + val));
        return this.getTarget();
    }

    public E notLeftLike(K key, String val) {
        predicateList.add(criteriaBuilder.notLike(this.get(key), "%" + val));
        return this.getTarget();
    }

    public E notLeftLike(K key, Function<Path, Expression> function, String val) {
        predicateList.add(criteriaBuilder.notLike(function.apply(this.get(key)), "%" + val));
        return this.getTarget();
    }

    public E rightLike(K key, String val) {
        predicateList.add(criteriaBuilder.like(this.get(key), val + "%"));
        return this.getTarget();
    }

    public E rightLike(K key, Function<Path, Expression> function, String val) {
        predicateList.add(criteriaBuilder.like(function.apply(this.get(key)), val + "%"));
        return this.getTarget();
    }

    public E notRightLike(K key, String val) {
        predicateList.add(criteriaBuilder.notLike(this.get(key), val + "%"));
        return this.getTarget();
    }

    public E notRightLike(K key, Function<Path, Expression> function, String val) {
        predicateList.add(criteriaBuilder.notLike(function.apply(this.get(key)), val + "%"));
        return this.getTarget();
    }

    public E like(K key, String val) {
        predicateList.add(criteriaBuilder.like(this.get(key), "%" + val + "%"));
        return this.getTarget();
    }

    public E like(K key, Function<Path, Expression> function, String val) {
        predicateList.add(criteriaBuilder.like(function.apply(this.get(key)), "%" + val + "%"));
        return this.getTarget();
    }

    public E isTrue(K... key) {
        for (K k : key) {
            predicateList.add(criteriaBuilder.isTrue(this.get(k)));
        }
        return this.getTarget();
    }


    public E isFalse(K... key) {
        for (K k : key) {
            predicateList.add(criteriaBuilder.isFalse(this.get(k)));
        }
        return this.getTarget();
    }


    public E isOfType(K key, Boolean type) {
        if (type == null) {
            return this.getTarget();
        }
        return type ? isTrue(key) : isFalse(key);
    }


    public <T extends Comparable> E between(K key, T val, T val2) {
        predicateList.add(criteriaBuilder.between(this.get(key), val, val2));
        return this.getTarget();
    }

    public E between(K key, String val, String val2) {
        predicateList.add(criteriaBuilder.between(this.get(key), val, val2));
        return this.getTarget();
    }

    public E between(K key, Function<Path, Expression> function, String val, String val2) {
        predicateList.add(criteriaBuilder.between(function.apply(this.get(key)), val, val2));
        return this.getTarget();
    }


    public <X extends Number> E ge(K key, X val) {
        predicateList.add(criteriaBuilder.ge(this.get(key), val));
        return this.getTarget();
    }

    public <X extends Number> E ge(K key, K key2) {
        predicateList.add(criteriaBuilder.ge(this.get(key), this.get(key2)));
        return this.getTarget();
    }

    public <X extends Number> E gt(K key, X val) {
        predicateList.add(criteriaBuilder.gt(this.get(key), val));
        return this.getTarget();
    }

    public <X extends Number> E gt(K key, K key2) {
        predicateList.add(criteriaBuilder.gt(this.get(key), this.get(key2)));
        return this.getTarget();
    }

    public E greaterThan(K key, String val) {
        predicateList.add(criteriaBuilder.greaterThan(this.get(key), val));
        return this.getTarget();
    }

    public E greaterThan(K key, Function<Path, Expression> function, String val) {
        predicateList.add(criteriaBuilder.greaterThan(function.apply(this.get(key)), val));
        return this.getTarget();
    }

    public E greaterThanThis(K key, K key2) {
        predicateList.add(criteriaBuilder.greaterThan(this.get(key), this.get(key2)));
        return this.getTarget();
    }

    public E greaterThanOrEqualTo(K key, String val) {
        predicateList.add(criteriaBuilder.greaterThanOrEqualTo(this.get(key), val));
        return this.getTarget();
    }

    public E greaterThanOrEqualTo(K key, LocalDateTime val) {
        predicateList.add(criteriaBuilder.greaterThanOrEqualTo(this.get(key), val));
        return this.getTarget();
    }

    public E greaterThanOrEqualTo(K key, LocalDate val) {
        predicateList.add(criteriaBuilder.greaterThanOrEqualTo(this.get(key), val));
        return this.getTarget();
    }

    public E greaterThanOrEqualTo(K key, Function<Path, Expression> function, String val) {
        predicateList.add(criteriaBuilder.greaterThanOrEqualTo(function.apply(this.get(key)), val));
        return this.getTarget();
    }

    public E greaterThanOrEqualToThis(K key, K key2) {
        predicateList.add(criteriaBuilder.greaterThanOrEqualTo(this.get(key), this.get(key2)));
        return this.getTarget();
    }

    public <X extends Number> E le(K key, X val) {
        predicateList.add(criteriaBuilder.le(this.get(key), val));
        return this.getTarget();
    }

    public <X extends Number> E le(K key, K key2) {
        predicateList.add(criteriaBuilder.le(this.get(key), this.get(key2)));
        return this.getTarget();
    }


    public <X extends Number> E lt(K key, X val) {
        predicateList.add(criteriaBuilder.lt(this.get(key), val));
        return this.getTarget();
    }

    public <X extends Number> E lt(K key, K key2) {
        predicateList.add(criteriaBuilder.lt(this.get(key), this.get(key2)));
        return this.getTarget();
    }

    public E lessThan(K key, String val) {
        predicateList.add(criteriaBuilder.lessThan(this.get(key), val));
        return this.getTarget();
    }

    public E lessThan(K key, Function<Path, Expression> function, String val) {
        predicateList.add(criteriaBuilder.lessThan(function.apply(this.get(key)), val));
        return this.getTarget();
    }

    public E lessThanThis(K key, K key2) {
        predicateList.add(criteriaBuilder.lessThan(this.get(key), this.get(key2)));
        return this.getTarget();
    }

    public E lessThanOrEqualTo(K key, String val) {
        predicateList.add(criteriaBuilder.lessThanOrEqualTo(this.get(key), val));
        return this.getTarget();
    }

    public E lessThanOrEqualTo(K key, Function<Path, Expression> function, String val) {
        predicateList.add(criteriaBuilder.lessThanOrEqualTo(function.apply(this.get(key)), val));
        return this.getTarget();
    }

    public E lessThanOrEqualTo(K key, LocalDate val) {
        predicateList.add(criteriaBuilder.lessThanOrEqualTo(this.get(key), val));
        return this.getTarget();
    }

    public E lessThanOrEqualTo(K key, LocalDateTime val) {
        predicateList.add(criteriaBuilder.lessThanOrEqualTo(this.get(key), val));
        return this.getTarget();
    }

    public E lessThanOrEqualToThis(K key, K key2) {
        predicateList.add(criteriaBuilder.lessThanOrEqualTo(this.get(key), this.get(key2)));
        return this.getTarget();
    }


    public E isNotNull(K key) {
        predicateList.add(criteriaBuilder.isNotNull(this.get(key)));
        return this.getTarget();
    }

    public E isNull(K key) {
        predicateList.add(criteriaBuilder.isNull(this.get(key)));
        return this.getTarget();
    }

    public E isNullOfType(K key, Boolean flag) {
        return flag ? isNull(key) : isNotNull(key);
    }

    public E isEmptyOfType(K key, Boolean flag) {
        return flag ? equal(key, "") : notEqual(key, "");
    }

    public E isNotEmpty(K key) {
        predicateList.add(criteriaBuilder.isNotEmpty(this.get(key)));
        return this.getTarget();
    }

    public E isEmpty(K key) {
        predicateList.add(criteriaBuilder.isEmpty(this.get(key)));
        return this.getTarget();
    }

    public E in(K key, Collection collection) {
        predicateList.add(criteriaBuilder.in(this.get(key)).value(collection));
        return this.getTarget();
    }

    public E notIn(K key, Collection collection) {
        predicateList.add(criteriaBuilder.in(this.get(key)).value(collection).not());
        return this.getTarget();
    }

    public E and() {
        if (this.predicateList.size() > 1) {
            Predicate predicate = criteriaBuilder.and(this.predicateList.toArray(new Predicate[0]));
            this.predicateList.clear();
            this.predicateList.add(predicate);
        }
        return this.getTarget();
    }

    public E or() {
        if (this.predicateList.size() > 1) {
            Predicate predicate = criteriaBuilder.or(this.predicateList.toArray(new Predicate[0]));
            this.predicateList.clear();
            this.predicateList.add(predicate);
        }
        return this.getTarget();
    }

    public E cacheAnd() {
        if (this.predicateList.size() > 1) {
            throw new RuntimeException("Cannot perform pre storage if condition is greater than one");
        }
        if (cachePredicate != null) {
            if (!this.predicateList.isEmpty()) {
                this.predicateList.add(cachePredicate);
                cachePredicate = criteriaBuilder.and(this.predicateList.toArray(new Predicate[0]));
            }
        } else {
            cachePredicate = this.predicateList.isEmpty() ? null : criteriaBuilder.and(this.predicateList.toArray(new Predicate[0]));
        }
        this.predicateList.clear();
        return this.getTarget();
    }

    public E cacheOr() {
        if (this.predicateList.size() > 1) {
            throw new RuntimeException("Cannot perform pre storage if condition is greater than one");
        }
        if (cachePredicate != null) {
            cachePredicate = criteriaBuilder.or(cachePredicate, this.predicateList.get(0));
        } else {
            cachePredicate = this.predicateList.isEmpty() ? null : this.predicateList.get(0);
        }
        this.predicateList.clear();
        return this.getTarget();
    }

    public E clear() {
        this.cachePredicate = null;
        this.predicateList.clear();
        return this.getTarget();
    }

    public Predicate endAnd() {
        this.cacheAnd();
        try {
            return cachePredicate;
        } finally {
            cachePredicate = null;
        }
    }

    public Predicate endOr() {
        this.cacheOr();
        try {
            return cachePredicate;
        } finally {
            cachePredicate = null;
        }
    }


    protected abstract Path get(K key);

}
